package com.bnz.search.service.impl;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.SortOptions;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch._types.aggregations.Buckets;
import co.elastic.clients.elasticsearch._types.aggregations.StringTermsBucket;
import co.elastic.clients.elasticsearch._types.query_dsl.MultiMatchQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.TermQuery;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.HighlightField;
import co.elastic.clients.elasticsearch.core.search.Hit;

import co.elastic.clients.elasticsearch.core.search.HitsMetadata;
import co.elastic.clients.elasticsearch.core.search.TotalHits;
import co.elastic.clients.json.JsonData;
import com.alibaba.fastjson.JSON;
import com.bnz.search.entity.ItemEntity;
import com.bnz.search.entity.ItemVo;
import com.bnz.search.entity.PageResult;
import com.bnz.search.service.SearchItemService;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


@Service
public class SearchItemServiceImpl implements SearchItemService {
    @Autowired
    private ElasticsearchClient client;
    // 1. 根据关键字进行查询
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Override
    public Map<String, Object> search(ItemVo vo) throws IOException {
        // 1.1 定义返回的结果集合
        Map<String, Object> resultMap = new HashMap<>();
        // 1.2 定义查询的商品集合
        List<ItemEntity> itemList = new ArrayList<>();
        // 1.3 定义要查询的对象
        SearchRequest request = null;
        // 1.4 定义组合过滤查询对象，将多字段匹配查询的结果放到里面，并且将以后的分类，品牌，价格等查询也放到里面
        List<Query> filterQuery = new ArrayList<>();
        // 定义分页参数
        int page = vo.getPage();
        int pageSize = vo.getPageSize();
        // 1.5 根据传入的关键字进行查询
        // 1.5.1 首先要进行判断，传入的关键字是否为空
        if (StringUtils.isNotBlank(vo.getKeywords())){
            // 1.5.2 传入的关键字有值，那么就根据关键字进行查询，并且是进行多字段匹配查询
            Query multiMatchQuery = MultiMatchQuery.of(mmq -> mmq.fields("brand", "category", "title").query(vo.getKeywords()))._toQuery();
            // 1.5.3 将得到的查询结果multiMatchQuery放到组合过滤查询的集合中
            filterQuery.add(multiMatchQuery);
            // 1.5.4 进行分类查询
            String category = vo.getCategory();
            if (StringUtils.isNotBlank(category)){
                // 1.5.4.1 得到分类查询的结果
                Query categoryQuery = TermQuery.of(tq -> tq.field("category").value(category))._toQuery();
                // 1.5.4.2 将这个结果categoryQuery放到组合过滤查询中
                filterQuery.add(categoryQuery);
            }
            // 1.5.5 进行品牌查询
            String brand = vo.getBrand();
            if (StringUtils.isNotBlank(brand)){
                // 1.5.5.1 得到品牌查询的结果
                Query brandQuery = TermQuery.of(tq -> tq.field("brand").value(brand))._toQuery();
                // 1.5.5.2 将查询结果放到组合过滤查询中
                filterQuery.add(brandQuery);
            }
            // 1.5.6 进行规格查询
            List<String> spec = vo.getSpec();
            if (spec != null){
                // 1.5.6.1 遍历规格
                for (String s : spec) {
                    // 1.5.6.2 对规格以：进行拆分
                    String[] split = s.split(":");
                    // 1.5.6.3 得到规格查询结果
                    Query specQuery = TermQuery.of(tq -> tq.field("specMap." + split[0] + ".keyword").value(split[1]))._toQuery();
                    // 1.5.6.4 将查询结果放到组合过滤查询中
                    filterQuery.add(specQuery);
                }
            }
            // 1.5.7 进行价格区间查询
            String price = vo.getPrice();
            // 1.5.7.1 判断价格区间是否有值
            if (StringUtils.isNotBlank(price)){
                // 1.5.7.2 有值就以-拆分价格区间
                String[] split = price.split("-");
                // 1.5.7.3 如果split[1]为*就说明是最后一个价格区间，这时候只需要大于split[0]就好了
                if (split[1].equals("*")){
                    Query priceQuery = RangeQuery.of(rq -> rq.field("price").gte(JsonData.of(split[0])))._toQuery();
                    // 1.5.7 将结果放到组合过滤查询中
                    filterQuery.add(priceQuery);
                }
                // 1.5.7.4 否则就要大于split[0]小于split[1]
                else {
                    Query priceQuery = RangeQuery.of(rq -> rq.field("price").gte(JsonData.of(split[0])).lte(JsonData.of(split[1])))._toQuery();
                    // 1.5.7 将结果放到组合过滤查询中
                    filterQuery.add(priceQuery);
                }


            }
            // 1.5.8 进行排序查询
            List<SortOptions> sortList = new ArrayList<>();
            // 1.5.8.1 得到排序
            String sort = vo.getSort();
            // 1.5.8.2 判断排序是否有值
            if (StringUtils.isNotBlank(sort)){
                // 1.5.8.3 有值，那么就以：拆分字符串，split[0]代表排序字段，split[1]代表按照升序asc还是降序desc进行排序
                String[] split = sort.split(":");
                // 1.5.8.4 进行升序排序
                if (split[1].equals("asc")){
                    SortOptions sortQuery = SortOptions.of(so -> so.field(f -> f.field(split[0]).order(SortOrder.Asc)));
                    // 1.5.8.5 将排序结果放到排序集合中
                    sortList.add(sortQuery);
                }
                // 1.5.8.6 进行降序排序
                else if (split[1].equals("desc")){
                    SortOptions sortQuery = SortOptions.of(so -> so.field(f -> f.field(split[0]).order(SortOrder.Desc)));
                    // 1.5.8.7 将排序结果放到排序集合中
                    sortList.add(sortQuery);
                }
            }
            // 还可以按照其他的进行排序查询，像销量，新品，评价。
            request = SearchRequest.of(sr->sr
                    // 1.5.9 进行组合过滤查询
                    .query(q->q.bool(b->b.filter(filterQuery)))
                    // 1.5.10 进行高亮查询，preTags是关键字的前缀，postTags是后缀，和关键字加起来就是整个高亮查询
                    .highlight(hl->hl.preTags("<span style='color:red'>").postTags("</span>").fields("title", HighlightField.of(hlf->hlf)))
                    // 1.5.11 进行分组查询，也就是聚合查询
                    .aggregations("categoryAgg",agg->agg.terms(aggr->aggr.field("category")))
                    // 1.5.12 进行排序查询
                    .sort(sortList)
                    // 1.5.13 进行分页查询
                    .from((page-1) * pageSize)
                    .size(pageSize)
            );
        }
        // 1.5. 如果为空就查询所有的数据
        else {
            request = SearchRequest.of(sr->sr.index("item").query(q->q.matchAll(ma->ma)));
        }

        // 1.6 返回查询响应的结果对象
        SearchResponse<ItemEntity> search = client.search(request, ItemEntity.class);
        // 1.7 得到返回的命中对象
        HitsMetadata<ItemEntity> hits = search.hits();
        // 1.8 得到返回的命中总记录数，总页数
        TotalHits total = hits.total();
        int totalPage = (int) Math.ceil(total.value() / (double)pageSize);
        // 1.9 处理高亮查询和多字段匹配查询的结果
        for (Hit<ItemEntity> hit : hits.hits()) {
            // 先得到原始数据
            ItemEntity itemEntity = hit.source();
            // 得到高亮查询的结果
            Map<String, List<String>> highlight = hit.highlight();
            // 根据高亮查询的key，得到相应的数据
            List<String> titles = highlight.get("title");
            // 对这个结果进行判断，如果有值，说明高亮查询成功，就将原数据替换成高亮查询的数据
            if (titles != null && titles.size() > 0){
                itemEntity.setTitle(titles.get(0));
            }
            // 将数据重新放到商品集合中
            itemList.add(itemEntity);
        }

        // 1.10 得到聚合查询的数据
        // 1.10.1 先得到所有的分组
        Map<String, Aggregate> aggregations = search.aggregations();
        // 1.10.2 根据key得到对应的某一组数据
        Aggregate categoryAgg = aggregations.get("categoryAgg");
        // 1.10.3 获取分组的数据
        List<String> categories = new ArrayList<>();
        // 1.10.4 判断如果有数据，那么久遍历数据并放入到categories集合中
        if (categoryAgg != null){
            // 1.10.4.1 将数据放到一个buckets桶中
            Buckets<StringTermsBucket> buckets = categoryAgg.sterms().buckets();
            // 1.10.4.2 遍历分组数据，并且其key存放在categories集合中
            for (StringTermsBucket bucket : buckets.array()) {
                categories.add(bucket.key());
            }
        }
        // 1.11 定义一个category用来接收分类的值
        String category = "";
        // 1.11.1 如果还没分类，将原来的值赋给category
        if (StringUtils.isNotBlank(vo.getCategory())){
            category = vo.getCategory();
        }
        // 1.11.2 如果进行了分类，就将分好类的数据传给category
        else{
            if (categories != null && categories.size() > 0){
                category = categories.get(0);
            }
        }
        // 1.12 在redis中根据分类名找到模板id，在找到对应的品牌和规格列表
        Map brandAndSpecMap = findBrandandSpecList(category);
        // 1.13 定义分页结果对象
        PageResult pr = new PageResult(page,total.value(),totalPage);
        // 1.14 将商品集合里面的数据添加到结果集合
        resultMap.put("itemList",itemList);
        // 1.15 将分类查询的结果放到结果集合中
        resultMap.put("categoryList",categories);
        // 1.16 将品牌和规格查询的结果放到结果集合中
        resultMap.putAll(brandAndSpecMap);
        // 1.17 将分页结果放到结果集合中
        resultMap.put("pr",pr);
        // 1.18 返回结果
        return resultMap;
    }

    // 在redis中根据分类名找到模板id，在找到对应的品牌和规格列表
    private Map findBrandandSpecList(String category) {
        Map map = new HashMap();
        // 1. 先判断是否存在分类名
        if (StringUtils.isNotBlank(category)){
            // 2. 存在就根据分类名得到模板id
            BoundHashOperations<String, String, String> itemCatList = redisTemplate.boundHashOps("itemCatList");
            String typeId = itemCatList.get(category);
            // 3. 根据模板id得到品牌列表
            BoundHashOperations<String, String, String> brandList = redisTemplate.boundHashOps("brandList");
            String brandListStr = brandList.get(typeId);
            // 3.1 将得到的品牌列表放到一个集合中
            List<Map> brands = JSON.parseArray(brandListStr, Map.class);
            // 4. 根据模板id得到规格和规格选项列表
            BoundHashOperations<String, String, String> specList = redisTemplate.boundHashOps("specList");
            String specListStr = specList.get(typeId);
            // 4.1 将得到的规格列表放到一个集合中
            List<Map> specs = JSON.parseArray(specListStr, Map.class);
            // 5. 将得到的品牌和规格都放到map中
            map.put("brandList",brands);
            map.put("specList",specs);
        }else {
            map.put("brandList",new ArrayList<>());
            map.put("specList",new ArrayList<>());
        }
        return map;
    }
}
